<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category  Zend
 * @package   Zend_Measure
 * @copyright Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd     New BSD License
 * @version   $Id: Number.php 16220 2009-06-21 19:49:21Z thomas $
 */

/**
 * Implement needed classes
 */
require_once 'Zend/Measure/Abstract.php';
require_once 'Zend/Locale.php';

/**
 * Class for handling number conversions
 *
 * This class can only handle numbers without precission
 *
 * @category   Zend
 * @package    Zend_Measure
 * @subpackage Zend_Measure_Number
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Measure_Number extends Zend_Measure_Abstract
{
	const STANDARD = 'DECIMAL';

	const BINARY      = 'BINARY';
	const TERNARY     = 'TERNARY';
	const QUATERNARY  = 'QUATERNARY';
	const QUINARY     = 'QUINARY';
	const SENARY      = 'SENARY';
	const SEPTENARY   = 'SEPTENARY';
	const OCTAL       = 'OCTAL';
	const NONARY      = 'NONARY';
	const DECIMAL     = 'DECIMAL';
	const DUODECIMAL  = 'DUODECIMAL';
	const HEXADECIMAL = 'HEXADECIMAL';
	const ROMAN       = 'ROMAN';

	/**
	 * Calculations for all number units
	 *
	 * @var array
	 */
	protected $_units = array(
        'BINARY'      => array(2,  '⑵'),
        'TERNARY'     => array(3,  '⑶'),
        'QUATERNARY'  => array(4,  '⑷'),
        'QUINARY'     => array(5,  '⑸'),
        'SENARY'      => array(6,  '⑹'),
        'SEPTENARY'   => array(7,  '⑺'),
        'OCTAL'       => array(8,  '⑻'),
        'NONARY'      => array(9,  '⑼'),
        'DECIMAL'     => array(10, '⑽'),
        'DUODECIMAL'  => array(12, '⑿'),
        'HEXADECIMAL' => array(16, '⒃'),
        'ROMAN'       => array(99, ''),
        'STANDARD'    => 'DECIMAL'
        );

        /**
         * Definition of all roman signs
         *
         * @var array $_roman
         */
        private static $_roman = array(
        'I' => 1,
        'A' => 4,
        'V' => 5,
        'B' => 9,
        'X' => 10,
        'E' => 40,
        'L' => 50,
        'F' => 90,
        'C' => 100,
        'G' => 400,
        'D' => 500,
        'H' => 900,
        'M' => 1000,
        'J' => 4000,
        'P' => 5000,
        'K' => 9000,
        'Q' => 10000,
        'N' => 40000,
        'R' => 50000,
        'W' => 90000,
        'S' => 100000,
        'Y' => 400000,
        'T' => 500000,
        'Z' => 900000,
        'U' => 1000000
        );

        /**
         * Convertion table for roman signs
         *
         * @var array $_romanconvert
         */
        private static $_romanconvert = array(
        '/_V/' => '/P/',
        '/_X/' => '/Q/',
        '/_L/' => '/R/',
        '/_C/' => '/S/',
        '/_D/' => '/T/',
        '/_M/' => '/U/',
        '/IV/' => '/A/',
        '/IX/' => '/B/',
        '/XL/' => '/E/',
        '/XC/' => '/F/',
        '/CD/' => '/G/',
        '/CM/' => '/H/',
        '/M_V/'=> '/J/',
        '/MQ/' => '/K/',
        '/QR/' => '/N/',
        '/QS/' => '/W/',
        '/ST/' => '/Y/',
        '/SU/' => '/Z/'
        );

        /**
         * Zend_Measure_Abstract is an abstract class for the different measurement types
         *
         * @param  integer            $value  Value
         * @param  string             $type   (Optional) A Zend_Measure_Number Type
         * @param  string|Zend_Locale $locale (Optional) A Zend_Locale
         * @throws Zend_Measure_Exception When language is unknown
         * @throws Zend_Measure_Exception When type is unknown
         */
        public function __construct($value, $type, $locale = null)
        {
        	if (($type !== null) and (Zend_Locale::isLocale($type, null, false))) {
        		$locale = $type;
        		$type = null;
        	}

        	if ($locale === null) {
        		$locale = new Zend_Locale();
        	}

        	if (!Zend_Locale::isLocale($locale, true, false)) {
        		if (!Zend_Locale::isLocale($locale, true, false)) {
        			require_once 'Zend/Measure/Exception.php';
        			throw new Zend_Measure_Exception("Language (" . (string) $locale . ") is unknown");
        		}

        		$locale = new Zend_Locale($locale);
        	}

        	$this->_locale = (string) $locale;

        	if ($type === null) {
        		$type = $this->_units['STANDARD'];
        	}

        	if (isset($this->_units[$type]) === false) {
        		require_once 'Zend/Measure/Exception.php';
        		throw new Zend_Measure_Exception("Type ($type) is unknown");
        	}

        	$this->setValue($value, $type, $this->_locale);
        }

        /**
         * Set a new value
         *
         * @param  integer            $value  Value
         * @param  string             $type   (Optional) A Zend_Measure_Number Type
         * @param  string|Zend_Locale $locale (Optional) A Zend_Locale Type
         * @throws Zend_Measure_Exception
         */
        public function setValue($value, $type = null, $locale = null)
        {
        	if (empty($locale)) {
        		$locale = $this->_locale;
        	}

        	if (empty($this->_units[$type])) {
        		require_once 'Zend/Measure/Exception.php';
        		throw new Zend_Measure_Exception('unknown type of number:' . $type);
        	}

        	switch($type) {
        		case 'BINARY':
        			preg_match('/[01]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'TERNARY':
        			preg_match('/[012]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'QUATERNARY':
        			preg_match('/[0123]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'QUINARY':
        			preg_match('/[01234]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'SENARY':
        			preg_match('/[012345]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'SEPTENARY':
        			preg_match('/[0123456]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'OCTAL':
        			preg_match('/[01234567]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'NONARY':
        			preg_match('/[012345678]+/', $value, $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'DUODECIMAL':
        			preg_match('/[0123456789AB]+/', strtoupper($value), $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'HEXADECIMAL':
        			preg_match('/[0123456789ABCDEF]+/', strtoupper($value), $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		case 'ROMAN':
        			preg_match('/[IVXLCDM_]+/', strtoupper($value), $ergebnis);
        			$value = $ergebnis[0];
        			break;

        		default:
        			try {
        				$value = Zend_Locale_Format::getInteger($value, array('locale' => $locale));
        			} catch (Exception $e) {
        				require_once 'Zend/Measure/Exception.php';
        				throw new Zend_Measure_Exception($e->getMessage());
        			}
        			if (call_user_func(Zend_Locale_Math::$comp, $value, 0) < 0) {
        				$value = call_user_func(Zend_Locale_Math::$sqrt, call_user_func(Zend_Locale_Math::$pow, $value, 2));
        			}
        			break;
        	}

        	$this->_value = $value;
        	$this->_type  = $type;
        }

        /**
         * Convert input to decimal value string
         *
         * @param  integer $input Input string
         * @param  string  $type  Type from which to convert to decimal
         * @return string
         */
        private function _toDecimal($input, $type)
        {
        	$value = '';
        	// Convert base xx values
        	if ($this->_units[$type][0] <= 16) {
        		$split  = str_split($input);
        		$length = strlen($input);
        		for ($x = 0; $x < $length; ++$x) {
        			$split[$x] = hexdec($split[$x]);
        			$value     = call_user_func(Zend_Locale_Math::$add, $value,
        			call_user_func(Zend_Locale_Math::$mul, $split[$x],
        			call_user_func(Zend_Locale_Math::$pow, $this->_units[$type][0], ($length - $x - 1))));
        		}
        	}

        	// Convert roman numbers
        	if ($type === 'ROMAN') {
        		$input = strtoupper($input);
        		$input = preg_replace(array_keys(self::$_romanconvert), array_values(self::$_romanconvert), $input);

        		$split = preg_split('//', strrev($input), -1, PREG_SPLIT_NO_EMPTY);

        		for ($x =0; $x < sizeof($split); $x++) {
        			if ($split[$x] == '/') {
        				continue;
        			}

        			$num = self::$_roman[$split[$x]];
        			if (($x > 0 and ($split[$x-1] != '/') and ($num < self::$_roman[$split[$x-1]]))) {
        				$num -= $num;
        			}

        			$value += $num;
        		}

        		str_replace('/', '', $value);
        	}

        	return $value;
        }

        /**
         * Convert input to type value string
         *
         * @param  integer $value Input string
         * @param  string  $type  Type to convert to
         * @return string
         * @throws Zend_Measure_Exception When more than 200 digits are calculated
         */
        private function _fromDecimal($value, $type)
        {
        	$tempvalue = $value;
        	if ($this->_units[$type][0] <= 16) {
        		$newvalue = '';
        		$count    = 200;
        		$base     = $this->_units[$type][0];

        		while (call_user_func(Zend_Locale_Math::$comp, $value, 0, 25) <> 0) {
        			$target = call_user_func(Zend_Locale_Math::$mod, $value, $base);

        			$newvalue = strtoupper(dechex($target)) . $newvalue;

        			$value = call_user_func(Zend_Locale_Math::$sub, $value, $target, 0);
        			$value = call_user_func(Zend_Locale_Math::$div, $value, $base, 0);

        			--$count;
        			if ($count === 0) {
        				require_once 'Zend/Measure/Exception.php';
        				throw new Zend_Measure_Exception("Your value '$tempvalue' cannot be processed because it extends 200 digits");
        			}
        		}

        		if ($newvalue === '') {
        			$newvalue = '0';
        		}
        	}

        	if ($type === 'ROMAN') {
        		$i        = 0;
        		$newvalue = '';
        		$romanval = array_values(array_reverse(self::$_roman));
        		$romankey = array_keys(array_reverse(self::$_roman));
        		$count    = 200;
        		while (call_user_func(Zend_Locale_Math::$comp, $value, 0, 25) <> 0) {
        			while ($value >= $romanval[$i]) {
        				$value    -= $romanval[$i];
        				$newvalue .= $romankey[$i];

        				if ($value < 1) {
        					break;
        				}

        				--$count;
        				if ($count === 0) {
        					require_once 'Zend/Measure/Exception.php';
        					throw new Zend_Measure_Exception("Your value '$tempvalue' cannot be processed because it extends 200 digits");
        				}
        			}

        			$i++;
        		}

        		$newvalue = str_replace('/', '', preg_replace(array_values(self::$_romanconvert), array_keys(self::$_romanconvert), $newvalue));
        	}

        	return $newvalue;
        }

        /**
         * Set a new type, and convert the value
         *
         * @param  string $type New type to set
         * @throws Zend_Measure_Exception When a unknown type is given
         * @return void
         */
        public function setType($type)
        {
        	if (empty($this->_units[$type]) === true) {
        		require_once 'Zend/Measure/Exception.php';
        		throw new Zend_Measure_Exception('Unknown type of number:' . $type);
        	}

        	$value = $this->_toDecimal($this->getValue(-1), $this->getType(-1));
        	$value = $this->_fromDecimal($value, $type);

        	$this->_value = $value;
        	$this->_type  = $type;
        }

        /**
         * Alias function for setType returning the converted unit
         * Default is 0 as this class only handles numbers without precision
         *
         * @param  string  $type  Type to convert to
         * @param  integer $round (Optional) Precision to add, will always be 0
         * @return string
         */
        public function convertTo($type, $round = 0, $locale = null)
        {
        	$this->setType($type);
        	return $this->toString($round, $locale);
        }
}
